package org.hawk.gwtppc.webtest.common;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.EventListener;

/**
 * Reporting class for testcases. Users should not create instances of this class
 * and usually even shouldn't manage reports from {@link TestSuite}.
 *   
 * @author alex.bereznevatiy@gmail.com
 */
public class Report {
	private List<String> passed;
	private List<String> failed;
	private List<String> skipped;
	
	private String currentSuite;

	Report() {
		passed = new ArrayList<String>();
		failed = new ArrayList<String>();
		skipped = new ArrayList<String>();
	}
	/**
	 * Starts execution of the suite
	 * @param name
	 */
	public void startSuite(String name) {
		currentSuite = name;
	}
	
	/**
	 * Signatures end of test suite execution.
	 */
	public void endSuite() {
		currentSuite = null;
	}
	
	/**
	 * Reports success of test execution. 
	 * @param test
	 */
	public void success(Test test) {
		passed.add(createTestResult(test));
	}
	
	private String createTestResult(Test test) {
		StringBuilder result = new StringBuilder();
		if (currentSuite != null && !currentSuite.isEmpty()) {
			result.append(currentSuite);
			result.append('.');
		}
		result.append(test.getName());
		
		return result.toString();
	}
	/**
	 * Reports failure of test execution (with exception reason)
	 * @param test
	 * @param reason
	 */
	public void failure(Test test, Throwable reason) {
		failed.add(createTestResult(test));
		printError(reason);
	}
	/**
	 * Reports skipping of test passed with message reason form the second argument.
	 * @param test
	 * @param message
	 */
	public void skip(Test test, String message) {
		skipped.add(createTestResult(test) + (message == null ? "" : ": " + message) );
	};

	private void printError(Throwable th) {
		StringBuilder errorString = new StringBuilder();
		errorString.append("<pre>");
		errorString.append(th.toString());
		
		StackTraceElement[] trace = th.getStackTrace();
        for (int i=0; i < trace.length; i++) {
        	errorString.append("\n\tat " + trace[i]);
        }

        if (th.getCause() != null) {
        	printErrorCause(errorString, th.getCause(), trace);
        }
		
		errorString.append("</pre>");
		failed.add(errorString.toString());
	}
	
	private static void printErrorCause(StringBuilder errorString, Throwable th, StackTraceElement[] causedTrace) {
		StackTraceElement[] trace = th.getStackTrace();
        int m = trace.length-1, n = causedTrace.length-1;
        while (m >= 0 && n >=0 && trace[m].equals(causedTrace[n])) {
            m--; n--;
        }
        int framesInCommon = trace.length - 1 - m;

        errorString.append("\nCaused by: " + th.toString());

        for (int i=0; i <= m; i++) {
        	errorString.append("\n\tat " + trace[i]);
        }
        if (framesInCommon != 0) {
        	errorString.append("\n\t... " + framesInCommon + " more");
        }

        if (th.getCause() != null) {
        	printErrorCause(errorString, th.getCause(), trace);
        }
	}
	/**
	 * Prints result to the output device.
	 * When running with rhino shell the result will be written to the stdout.
	 */
	void print() {
		Document doc = Document.get();
		DivElement div = doc.createDivElement();
		div.setInnerText(String.valueOf((failed.size()/2) + passed.size() + skipped.size()) + 
				" tests run, " + passed.size() + " passed, " + (failed.size()/2) + " failed, " + skipped.size() + " skipped." );
		div.setClassName("header");
		doc.getBody().appendChild(div);
		
		Iterator<String> failedIterator = failed.iterator();
		
		while (failedIterator.hasNext()) {
			String line = failedIterator.next();
			println(line, "test-failed", failedIterator.hasNext() ? failedIterator.next() : null);
		}
		
		Iterator<String> passedIterator = passed.iterator();
		
		while (passedIterator.hasNext()) {
			println(passedIterator.next(), "test-passed", null);
		}
		
		Iterator<String> skippedIterator = skipped.iterator();
		
		while (skippedIterator.hasNext()) {
			println(skippedIterator.next(), "test-skipped", null);
		}
	}
	
	private static void println(String line, String className, String exception) {
		Document doc = Document.get();
		DivElement div = doc.createDivElement();
		div.setInnerText(line);
		div.setClassName(className);
		
		if (exception == null || exception.isEmpty()) {
			doc.getBody().appendChild(div);
			return;
		}
		
		final Element expand = createExpandButton(doc, div);
		
		final DivElement traces = createTracesList(exception, doc, div);
		
		doc.getBody().appendChild(div);
		
		assignExpandListener(expand, traces);
	}
	
	private static void assignExpandListener(final Element expand,
			final DivElement traces) {
		com.google.gwt.user.client.Element castedElem = (com.google.gwt.user.client.Element) expand;
	    DOM.setEventListener(castedElem, new EventListener() {
			
			public void onBrowserEvent(Event event) {
				if (event.getTypeInt() != Event.ONCLICK) {
					return;
				}
				if (traces.getClassName().equals("traces-collapsed")) {
					expand.setInnerText("-");
					traces.setClassName("traces-expanded");
				} else {
					expand.setInnerText("+");
					traces.setClassName("traces-collapsed");
				}
			}
		});
	    
		DOM.sinkEvents(castedElem, Event.ONCLICK | DOM.getEventsSunk(castedElem));
	}
	
	private static DivElement createTracesList(String exception, Document doc,
			DivElement div) {
		final DivElement traces = doc.createDivElement();
		traces.setClassName("traces-collapsed");
		traces.setInnerHTML(exception);
		div.appendChild(traces);
		return traces;
	}
	
	private static Element createExpandButton(Document doc, DivElement div) {
		final Element expand = doc.createDivElement();
		expand.setInnerText("+");
		expand.setAttribute("unselectable", "on");
		expand.setClassName("test-expand-button");
		div.appendChild(expand);
		return expand;
	}
}
